﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CosiNet.VM
{
    /// <summary>
    /// A COSIL Virtual Machine for the running of COSIL bytecode
    /// </summary>
    public class VM
    {
        /// <summary>
        /// A new COSIL VM
        /// </summary>
        /// <param name="MemSize">Size of virtual memory in bytes (must be larger than the application size)</param>
        public VM(int memSize, byte[] application)
        {
            ram = new RAM(memSize);
            sixbits = new bool[9];
            twobits = new bool[2];
            paras = new int[5];
            IP = 0;
            PC = 1;
            stdlib.KernelInterrupt.parent = this;
            LoadApp(application);
        }

        private enum AddressingMode
        {
            RegReg,
            ValReg,
            RegVal,
            ValVal
        }

        private AddressingMode opMode;
        private byte curOp;
        public void Run()
        {
            while (ram.memory[IP] != 0x00)
            {
                byte opcode = (byte)GetFirstSix(ram.memory[IP]);
                GetAdMode(ram.memory[IP]);
                ParseOpcode(opcode);
                IP = PC;
                PC++;
            }
        }
        private bool lastLogic;
        private byte newIP;
        private int[] paras;
        private byte ParseOpcode(byte opcode)
        {
            #region [COMPLETE] MOV (Move, 0x01)
            if (opcode == (byte)0x01)
            {
                // Parameter 1: Destination, must be between 0xF0 and 0xFF
                paras[1] = ram.memory[(IP + 1)];
                if (!(paras[1] > 0xEF))
                {
                    //Not a register, throw an error
                    throw new Exception("[CRITICAL ERROR] The value at " + (IP + 1) + "(" + paras[1] + ") does not refer to a register.");
                }
                if (opMode == AddressingMode.RegVal)
                {
                    // Parameter 2: Value, just set the register to the value
                    paras[2] = Get32BitParameter(IP + 2);
                    SetRegister((byte)paras[1], paras[2]);
#if DEBUG
                    Globals.console.WriteLine("MOV " + paras[1].ToString() + " " + paras[2].ToString() + "  : " + AL.ToString());
#endif
                }
                else if (opMode == AddressingMode.RegReg)
                {
                    // Parameter 2: Register, set the register to the value in the second register
                    paras[2] = GetRegister(ram.memory[(IP + 2)]);
                    SetRegister((byte)paras[1], paras[2]);
                }
                else
                {
                    // Parameter 2: WRONG ADDRESSING MODE
                    throw new Exception("[CRITICAL ERROR] The adressing mode at "+IP+" ("+adMode+") is invalid for the MOV (0x01) instruction.");
                }

                PC += 5;
            }
            #endregion
            #region [COMPLETE] MOM/E (Move Memory, 0x3A & 0x3B)
            else if (opcode == (byte)0x3A)
            {
                // Parameter 2: Destination
                paras[2] = Get32BitParameter(IP + 2);
                // Parameter 1: Source
                if (opMode == AddressingMode.RegVal)
                {
                    paras[1] = GetRegister(ram.memory[IP + 1]);
                    ram.SetByte(paras[2], (byte)paras[1]);
                }
                else if (opMode == AddressingMode.ValVal)
                {
                    paras[1] = ram.memory[IP + 1];
                    ram.SetByte(paras[2], (byte)paras[1]);
                }
                PC += 5;
            }
            else if (opcode == (byte)0x3B)
            {
                // Parameter 2: Source
                paras[2] = Get32BitParameter(IP + 2);
                if (opMode == AddressingMode.ValVal)
                {
                    // Parameter 1: Destination - Register, put the value in the register into the memory
                    paras[1] = ram.memory[IP + 1];
                    SetRegister((byte)paras[1], ram.memory[paras[2]]);
                    
                }
                else
                {
                    // WRONG ADDRESSING MODE
                    throw new Exception("[CRITICAL ERROR] The adressing mode at " + IP + " (" + adMode + ") is invalid for the MOM (0x3B) instruction.");
                }

                PC += 5;
            }
            #endregion
            #region [COMPLETE] ADD (Add, 0x04)
            else if (opcode == (byte)0x04)
            {
                if (opMode == AddressingMode.RegVal)
                {
                    SetRegister(ram.memory[IP + 1], (GetRegister(ram.memory[IP + 1]) + Get32BitParameter(IP + 2)));
                }
                else if (opMode == AddressingMode.RegReg)
                {
                    SetRegister(ram.memory[IP + 1], (GetRegister(ram.memory[IP + 1]) + GetRegister(ram.memory[IP + 2])));
                }
                else
                {
                    throw new Exception("[CRITICAL ERROR] The instruction ADD does not support addressing mode: " + adMode);
                }
                PC += 5;
            }
            #endregion
            #region [COMPLETE] JMP (Jump, 0x10)
            else if (opcode == (byte)0x10)
            {
                // Parameter 1: Memory Location, must be between 0xF0 and 0xFF
                paras[1] = ram.memory[(IP + 1)];
                PC = (byte)paras[1];
            }
            #endregion
            #region [COMPLETE] CLL (Call, 0x11)
            else if (opcode == (byte)0x11)
            {
                // Parameter 1: Memory Location
                // Add the next position in ram to the call stack
                CallStack.Call((PC + 5));
                // Jump to the location
                paras[1] = ram.memory[(IP + 1)];
                PC = (byte)paras[1];
#if DEBUG
                Globals.console.WriteLine("CLL " + paras[1].ToString());
#endif
            }
            #endregion
            #region [COMPLETE] RET (Return, 0x12)
            else if (opcode == (byte)0x12)
            {
                // Get the last location from the call stack and jump to it
                PC = (byte)CallStack.Return();
#if DEBUG
                Globals.console.WriteLine("RET " + PC.ToString());
#endif
            }
            #endregion

            #region [COMPLETE] KEI (Kernel Interrupt, 0x2B)
            else if (opcode == (byte)0x2B)
            {
                // Parameter 1: Kernel Interrupt to call
                paras[1] = ram.memory[(IP + 1)];
                stdlib.KernelInterrupt.Handle(paras[1]);
                PC += 5;
            }
            #endregion
            else
            {
                throw new Exception("[ERROR] The instruction at " + IP + " (" + opcode + ") is not supported by this COSIL VM");
            }
            newIP = 0;
            return newIP;
        }

        private int adMode = 0;
        private void GetAdMode(byte b)
        {
            adMode = GetLastTwo(b);
            if (adMode == 0)
            {
                opMode = AddressingMode.RegReg;
            }
            else if (adMode == 1)
            {
                opMode = AddressingMode.ValReg;
            }
            else if (adMode == 2)
            {
                opMode = AddressingMode.RegVal;
            }
            else if (adMode == 3)
            {
                opMode = AddressingMode.ValVal;
            }
            else
            {
                throw new Exception("[CRITICAL ERROR] The addressing mode at " + IP + " (" +adMode+") is invalid.");
            }
        }
        private int GetRegister(byte register)
        {
            if (register == (byte)0xF0)
                return (int)PC;
            else if (register == (byte)0xF1)
                return (int)IP;
            else if (register == (byte)0xF2)
                return (int)SP;
            else if (register == (byte)0xF3)
                return (int)SS;
            else if (register == (byte)0xF4)
                return GetSplit('A');
            else if (register == (byte)0xF5)
                return AL;
            else if (register == (byte)0xF6)
                return AH;
            else if (register == (byte)0xF7)
                return GetSplit('B');
            else if (register == (byte)0xF8)
                return BL;
            else if (register == (byte)0xF9)
                return BH;
            else if (register == (byte)0xFA)
                return GetSplit('C');
            else if (register == (byte)0xFB)
                return CH;
            else if (register == (byte)0xFC)
                return CL;
            else if (register == (byte)0xFD)
                return X;
            else if (register == (byte)0xFE)
                return Y;
            else
                return 0;
        }
        private void SetRegister(byte register, int content)
        {
            if (register == (byte)0xF0)
                PC = (byte)content;
            else if (register == (byte)0xF1)
                IP = (byte)content;
            //ignore 0xF2 as it is SP and SP is read only
            else if (register == (byte)0xF3)
                SS = (byte)content;
            else if (register == (byte)0xF4)
                SetSplit('A', content);
            else if (register == (byte)0xF5)
                AL = (byte)content;
            else if (register == (byte)0xF6)
                AH = (byte)content;
            else if (register == (byte)0xF7)
                SetSplit('B', content);
            else if (register == (byte)0xF8)
                BL = (byte)content;
            else if (register == (byte)0xF9)
                BH = (byte)content;
            else if (register == (byte)0xFA)
                SetSplit('C', content);
            else if (register == (byte)0xFB)
                CL = (byte)content;
            else if (register == (byte)0xFC)
                CH = (byte)content;
            else if (register == (byte)0xFD)
                X = content;
            else if (register == (byte)0xFE)
                Y = content;
            else
                throw new Exception("ERROR: The register " + register + " is not a register.");
        }
        public void SetSplit(char register, int content)
        {
            byte lower;
            byte higher;
            if (content > 255)
            {
                lower = (byte)255;
                higher = (byte)(content - 255);
            }
            else
            {
                lower = (byte)content;
                if (register == 'A')
                {
                    AL = lower;
                }
                else if (register == 'B')
                {
                    BL = lower;
                }
                else if (register == 'C')
                {
                    CL = lower;
                }
            }
        }
        public int GetSplit(char register)
        {
            if (register == 'A')
            {
                return BitOps.combineBytes(AL, AH);
            }
            else if (register == 'B')
            {
                return BitOps.combineBytes(BL, BH);
            }
            else if (register == 'C')
            {
                return BitOps.combineBytes(CL, CH);
            }
            throw new Exception("There was an internal error and the VM has had to close. Please report this to the developers at http://cosinet.codeplex.com with the message: 'GetSplit(), bad register!'");
        }

        private int Get32BitParameter(int startingIndex)
        {
            byte[] seperate = new byte[4];
            //First we need to get the bytes to convert.
            for (int i = 0; i < 4; i++)
            {
                // Get each byte
                seperate[i] = ram.memory[startingIndex + i];
            }
            bool[] b1 = new bool[8];
            bool[] b2 = new bool[8];
            bool[] b3 = new bool[8];
            bool[] b4 = new bool[8];
            b1 = BitOps.getBinaryValue(seperate[0]);
            b2 = BitOps.getBinaryValue(seperate[1]);
            b3 = BitOps.getBinaryValue(seperate[2]);
            b4 = BitOps.getBinaryValue(seperate[3]);
            b1 = Conversions.BoolArray.JoinOn(b1, b2);
            b1 = Conversions.BoolArray.JoinOn(b1, b3);
            b1 = Conversions.BoolArray.JoinOn(b1, b4);
            return BitOps.getIntegerValue(b1);
        }

        private void LoadApp(byte[] application)
        {
            int i = 0;
            while (i < application.Length)
            {
                ram.memory[i] = application[i];
                i++;
            }
            ram.setLimit = (i + 1);
        }

        private bool GetBit(byte b, int bitNumber)
        {
            return ((int)b & (1 << bitNumber)) != 0;
        }

        private bool[] sixbits;
        private int GetFirstSix(byte b)
        {
            for (int i = 0; i < 6; i++)
            {
                sixbits[i] = GetBit(b, i);
            }
            return BitOps.getIntegerValue(sixbits);
        }
        private bool[] twobits;
        private int GetLastTwo(byte b)
        {
            byte c = 0;
            for (int i = 7; i > 5; i--)
            {
                twobits[c] = GetBit(b, i);
                c++;
            }
            return BitOps.getIntegerValue(twobits);
        }
        byte[] appCode;
        public RAM ram;
        #region Registers
        public byte PC;
        public byte IP;
        public byte SP;
        public byte SS;
        public byte AL;
        public byte AH;
        public byte BL;
        public byte BH;
        public byte CL;
        public byte CH;
        public Int32 X;
        public Int32 Y;
        #endregion

        #region Internals

        #endregion
    }
    static class BitOps
    {
        public static bool getBit(byte b, int bitNumber)
        {
            return ((int)b & (1 << bitNumber)) != 0;
        }
        public static int getIntegerValue(bool[] bits)
        {
            int ans = 0;
            for (int i = 0; i < bits.Length; i++)
            {
                if (bits[i] == true)
                {
                    if (Globals.cosmosMode)
                    {
                        ans += CosmosPlugs.Maths.Pow(2, i);
                    }
                    else
                    {
                        ans += (int)Math.Pow(2, i);
                    }
                }
            }
            return ans;
        }
        public static bool[] getBinaryValue(byte b)
        {
            bool[] ret = new bool[8];
            for (int i = 0; i < 8; i++)
            {
                ret[i] = getBit(b, i);
            }
            return ret;
        }
        public static int combineBytes(byte one, byte two)
        {
            bool[] sixteenBit = new bool[16];
            bool[] binaryOne = getBinaryValue(one);
            bool[] binaryTwo = getBinaryValue(two);
            sixteenBit = Conversions.BoolArray.JoinOn(binaryOne, binaryTwo);
            return getIntegerValue(sixteenBit);
        }
    }
}
